/*********************************************************************
 * Copyright (C) 2006 Andrew Khan
 * <p>
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * <p>
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * <p>
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 ***************************************************************************/

package jxl.write.biff;

import jxl.*;
import jxl.biff.CellReferenceHelper;
import jxl.biff.*;
import jxl.biff.formula.FormulaException;
import jxl.common.Assert;
import jxl.common.Logger;
import jxl.format.CellFormat;
import jxl.write.*;
import jxl.write.Boolean;
import jxl.write.Number;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TreeSet;

/**
 * A transient utility object used to copy sheets.   This 
 * functionality has been farmed out to a different class
 * in order to reduce the bloat of the WritableSheetImpl
 */
class WritableSheetCopier {
    private static Logger logger = Logger.getLogger(SheetCopier.class);

    private WritableSheetImpl fromSheet;
    private WritableSheetImpl toSheet;
    private WorkbookSettings workbookSettings;

    // Objects used by the sheet
    private TreeSet fromColumnFormats;
    private TreeSet toColumnFormats;
    private MergedCells fromMergedCells;
    private MergedCells toMergedCells;
    private RowRecord[] fromRows;
    private ArrayList fromRowBreaks;
    private ArrayList fromColumnBreaks;
    private ArrayList toRowBreaks;
    private ArrayList toColumnBreaks;
    private DataValidation fromDataValidation;
    private DataValidation toDataValidation;
    private SheetWriter sheetWriter;
    private ArrayList fromDrawings;
    private ArrayList toDrawings;
    private ArrayList toImages;
    private WorkspaceInformationRecord fromWorkspaceOptions;
    private PLSRecord fromPLSRecord;
    private PLSRecord toPLSRecord;
    private ButtonPropertySetRecord fromButtonPropertySet;
    private ButtonPropertySetRecord toButtonPropertySet;
    private ArrayList fromHyperlinks;
    private ArrayList toHyperlinks;
    private ArrayList validatedCells;
    private int numRows;
    private int maxRowOutlineLevel;
    private int maxColumnOutlineLevel;


    private boolean chartOnly;
    private FormattingRecords formatRecords;


    // Objects used to maintain state during the copy process
    private HashMap xfRecords;
    private HashMap fonts;
    private HashMap formats;

    public WritableSheetCopier(WritableSheet f, WritableSheet t) {
        fromSheet = (WritableSheetImpl) f;
        toSheet = (WritableSheetImpl) t;
        workbookSettings = toSheet.getWorkbook().getSettings();
        chartOnly = false;
    }

    void setColumnFormats(TreeSet fcf, TreeSet tcf) {
        fromColumnFormats = fcf;
        toColumnFormats = tcf;
    }

    void setMergedCells(MergedCells fmc, MergedCells tmc) {
        fromMergedCells = fmc;
        toMergedCells = tmc;
    }

    void setRows(RowRecord[] r) {
        fromRows = r;
    }

    void setValidatedCells(ArrayList vc) {
        validatedCells = vc;
    }

    void setRowBreaks(ArrayList frb, ArrayList trb) {
        fromRowBreaks = frb;
        toRowBreaks = trb;
    }

    void setColumnBreaks(ArrayList fcb, ArrayList tcb) {
        fromColumnBreaks = fcb;
        toColumnBreaks = tcb;
    }

    void setDrawings(ArrayList fd, ArrayList td, ArrayList ti) {
        fromDrawings = fd;
        toDrawings = td;
        toImages = ti;
    }

    void setHyperlinks(ArrayList fh, ArrayList th) {
        fromHyperlinks = fh;
        toHyperlinks = th;
    }

    void setWorkspaceOptions(WorkspaceInformationRecord wir) {
        fromWorkspaceOptions = wir;
    }

    void setDataValidation(DataValidation dv) {
        fromDataValidation = dv;
    }

    void setPLSRecord(PLSRecord plsr) {
        fromPLSRecord = plsr;
    }

    void setButtonPropertySetRecord(ButtonPropertySetRecord bpsr) {
        fromButtonPropertySet = bpsr;
    }

    void setSheetWriter(SheetWriter sw) {
        sheetWriter = sw;
    }


    DataValidation getDataValidation() {
        return toDataValidation;
    }

    PLSRecord getPLSRecord() {
        return toPLSRecord;
    }

    boolean isChartOnly() {
        return chartOnly;
    }

    ButtonPropertySetRecord getButtonPropertySet() {
        return toButtonPropertySet;
    }

    /**
     * Copies a sheet from a read-only version to the writable version.
     * Performs shallow copies
     */
    public void copySheet() {
        shallowCopyCells();

        // Copy the column formats
        Iterator cfit = fromColumnFormats.iterator();
        while (cfit.hasNext()) {
            ColumnInfoRecord cv = new ColumnInfoRecord
                    ((ColumnInfoRecord) cfit.next());
            toColumnFormats.add(cv);
        }

        // Copy the merged cells
        Range[] merged = fromMergedCells.getMergedCells();

        for (int i = 0; i < merged.length; i++) {
            toMergedCells.add(new SheetRangeImpl((SheetRangeImpl) merged[i],
                    toSheet));
        }

        try {
            RowRecord row = null;
            RowRecord newRow = null;
            for (int i = 0; i < fromRows.length; i++) {
                row = fromRows[i];

                if (row != null &&
                        (!row.isDefaultHeight() ||
                                row.isCollapsed())) {
                    newRow = toSheet.getRowRecord(i);
                    newRow.setRowDetails(row.getRowHeight(),
                            row.matchesDefaultFontHeight(),
                            row.isCollapsed(),
                            row.getOutlineLevel(),
                            row.getGroupStart(),
                            row.getStyle());
                }
            }
        } catch (RowsExceededException e) {
            // Handle the rows exceeded exception - this cannot occur since
            // the sheet we are copying from will have a valid number of rows
            Assert.verify(false);
        }

        // Copy the horizontal page breaks
        toRowBreaks = new ArrayList(fromRowBreaks);

        // Copy the vertical page breaks
        toColumnBreaks = new ArrayList(fromColumnBreaks);

        // Copy the data validations
        if (fromDataValidation != null) {
            toDataValidation = new DataValidation
                    (fromDataValidation,
                            toSheet.getWorkbook(),
                            toSheet.getWorkbook(),
                            toSheet.getWorkbook().getSettings());
        }

        // Copy the charts
        sheetWriter.setCharts(fromSheet.getCharts());

        // Copy the drawings
        for (Iterator i = fromDrawings.iterator(); i.hasNext(); ) {
            Object o = i.next();
            if (o instanceof jxl.biff.drawing.Drawing) {
                WritableImage wi = new WritableImage
                        ((jxl.biff.drawing.Drawing) o,
                                toSheet.getWorkbook().getDrawingGroup());
                toDrawings.add(wi);
                toImages.add(wi);
            }

            // Not necessary to copy the comments, as they will be handled by
            // the deep copy of the individual cells
        }

        // Copy the workspace options
        sheetWriter.setWorkspaceOptions(fromWorkspaceOptions);

        // Copy the environment specific print record
        if (fromPLSRecord != null) {
            toPLSRecord = new PLSRecord(fromPLSRecord);
        }

        // Copy the button property set
        if (fromButtonPropertySet != null) {
            toButtonPropertySet = new ButtonPropertySetRecord(fromButtonPropertySet);
        }

        // Copy the hyperlinks
        for (Iterator i = fromHyperlinks.iterator(); i.hasNext(); ) {
            WritableHyperlink hr = new WritableHyperlink
                    ((WritableHyperlink) i.next(), toSheet);
            toHyperlinks.add(hr);
        }
    }

    /**
     * Performs a shallow copy of the specified cell
     */
    private WritableCell shallowCopyCell(Cell cell) {
        CellType ct = cell.getType();
        WritableCell newCell = null;

        if (ct == CellType.LABEL) {
            newCell = new Label((LabelCell) cell);
        } else if (ct == CellType.NUMBER) {
            newCell = new Number((NumberCell) cell);
        } else if (ct == CellType.DATE) {
            newCell = new DateTime((DateCell) cell);
        } else if (ct == CellType.BOOLEAN) {
            newCell = new Boolean((BooleanCell) cell);
        } else if (ct == CellType.NUMBER_FORMULA) {
            newCell = new ReadNumberFormulaRecord((FormulaData) cell);
        } else if (ct == CellType.STRING_FORMULA) {
            newCell = new ReadStringFormulaRecord((FormulaData) cell);
        } else if (ct == CellType.BOOLEAN_FORMULA) {
            newCell = new ReadBooleanFormulaRecord((FormulaData) cell);
        } else if (ct == CellType.DATE_FORMULA) {
            newCell = new ReadDateFormulaRecord((FormulaData) cell);
        } else if (ct == CellType.FORMULA_ERROR) {
            newCell = new ReadErrorFormulaRecord((FormulaData) cell);
        } else if (ct == CellType.EMPTY) {
            if (cell.getCellFormat() != null) {
                // It is a blank cell, rather than an empty cell, so
                // it may have formatting information, so
                // it must be copied
                newCell = new Blank(cell);
            }
        }

        return newCell;
    }

    /**
     * Performs a deep copy of the specified cell, handling the cell format
     *
     * @param cell the cell to copy
     */
    private WritableCell deepCopyCell(Cell cell) {
        WritableCell c = shallowCopyCell(cell);

        if (c == null) {
            return c;
        }

        if (c instanceof ReadFormulaRecord) {
            ReadFormulaRecord rfr = (ReadFormulaRecord) c;
            boolean crossSheetReference = !rfr.handleImportedCellReferences
                    (fromSheet.getWorkbook(),
                            fromSheet.getWorkbook(),
                            workbookSettings);

            if (crossSheetReference) {
                try {
                    logger.warn("Formula " + rfr.getFormula() +
                            " in cell " +
                            CellReferenceHelper.getCellReference(cell.getColumn(),
                                    cell.getRow()) +
                            " cannot be imported because it references another " +
                            " sheet from the source workbook");
                } catch (FormulaException e) {
                    logger.warn("Formula  in cell " +
                            CellReferenceHelper.getCellReference(cell.getColumn(),
                                    cell.getRow()) +
                            " cannot be imported:  " + e.getMessage());
                }

                // Create a new error formula and add it instead
                c = new Formula(cell.getColumn(), cell.getRow(), "\"ERROR\"");
            }
        }

        // Copy the cell format
        CellFormat cf = c.getCellFormat();
        int index = ((XFRecord) cf).getXFIndex();
        WritableCellFormat wcf = (WritableCellFormat)
                xfRecords.get(new Integer(index));

        if (wcf == null) {
            wcf = copyCellFormat(cf);
        }

        c.setCellFormat(wcf);

        return c;
    }

    /**
     * Perform a shallow copy of the cells from the specified sheet into this one
     */
    void shallowCopyCells() {
        // Copy the cells
        int cells = fromSheet.getRows();
        Cell[] row = null;
        Cell cell = null;
        for (int i = 0; i < cells; i++) {
            row = fromSheet.getRow(i);

            for (int j = 0; j < row.length; j++) {
                cell = row[j];
                WritableCell c = shallowCopyCell(cell);

                // Encase the calls to addCell in a try-catch block
                // These should not generate any errors, because we are
                // copying from an existing spreadsheet.  In the event of
                // errors, catch the exception and then bomb out with an
                // assertion
                try {
                    if (c != null) {
                        toSheet.addCell(c);

                        // Cell.setCellFeatures short circuits when the cell is copied,
                        // so make sure the copy logic handles the validated cells
                        if (c.getCellFeatures() != null &
                                c.getCellFeatures().hasDataValidation()) {
                            validatedCells.add(c);
                        }
                    }
                } catch (WriteException e) {
                    Assert.verify(false);
                }
            }
        }
        numRows = toSheet.getRows();
    }

    /**
     * Perform a deep copy of the cells from the specified sheet into this one
     */
    void deepCopyCells() {
        // Copy the cells
        int cells = fromSheet.getRows();
        Cell[] row = null;
        Cell cell = null;
        for (int i = 0; i < cells; i++) {
            row = fromSheet.getRow(i);

            for (int j = 0; j < row.length; j++) {
                cell = row[j];
                WritableCell c = deepCopyCell(cell);

                // Encase the calls to addCell in a try-catch block
                // These should not generate any errors, because we are
                // copying from an existing spreadsheet.  In the event of
                // errors, catch the exception and then bomb out with an
                // assertion
                try {
                    if (c != null) {
                        toSheet.addCell(c);

                        // Cell.setCellFeatures short circuits when the cell is copied,
                        // so make sure the copy logic handles the validated cells
                        if (c.getCellFeatures() != null &
                                c.getCellFeatures().hasDataValidation()) {
                            validatedCells.add(c);
                        }
                    }
                } catch (WriteException e) {
                    Assert.verify(false);
                }
            }
        }
    }

    /**
     * Returns an initialized copy of the cell format
     *
     * @param cf the cell format to copy
     * @return a deep copy of the cell format
     */
    private WritableCellFormat copyCellFormat(CellFormat cf) {
        try {
            // just do a deep copy of the cell format for now.  This will create
            // a copy of the format and font also - in the future this may
            // need to be sorted out
            XFRecord xfr = (XFRecord) cf;
            WritableCellFormat f = new WritableCellFormat(xfr);
            formatRecords.addStyle(f);

            // Maintain the local list of formats
            int xfIndex = xfr.getXFIndex();
            xfRecords.put(new Integer(xfIndex), f);

            int fontIndex = xfr.getFontIndex();
            fonts.put(new Integer(fontIndex), new Integer(f.getFontIndex()));

            int formatIndex = xfr.getFormatRecord();
            formats.put(new Integer(formatIndex), new Integer(f.getFormatRecord()));

            return f;
        } catch (NumFormatRecordsException e) {
            logger.warn("Maximum number of format records exceeded.  Using " +
                    "default format.");

            return WritableWorkbook.NORMAL_STYLE;
        }
    }


    /**
     * Accessor for the maximum column outline level
     *
     * @return the maximum column outline level, or 0 if no outlines/groups
     */
    public int getMaxColumnOutlineLevel() {
        return maxColumnOutlineLevel;
    }

    /**
     * Accessor for the maximum row outline level
     *
     * @return the maximum row outline level, or 0 if no outlines/groups
     */
    public int getMaxRowOutlineLevel() {
        return maxRowOutlineLevel;
    }
}
